The UIScreen class has been a part of iOS since the beginning. Its mainScreen
class method gives you the screen of the device you're running on,
which you can query for geometry information such as its bounds or
frame.
In iOS 3.2, UIScreen gets a new class method, screens,
which returns an array of all currently connected screens, including
the iPad's own screen. This is the key to accessing an attached
external screen. If it contains more than one item, all but the first
are external screens. Also new in iOS 3.2 is the ability to ask any
screen which resolutions it supports via the availableModes method, along with the currentMode method for determining and setting which resolution is in use.
You can move any of your content to an external screen simply by creating a new UIWindow object, adding your views to it, and setting its screen property to point to the external screen.
1. Extending the Video App to Handle an External Screen
When using an external
screen, you need to consider how to properly handle when the user plugs
in or unplugs a screen. To help with this, UIScreen defines a few notifications that let you know when a screen is connected or disconnected, so you can act accordingly.
In this section, we'll extend our VideoToy
project so that if a screen is connected, the video you choose will
play on it; if you disconnect the screen, the video will continue
playing on the device. This will require a bit of extra bookkeeping on
our part—we'll need to keep track of the currently selected video and
its corresponding views, so that we can switch things around as the
external screen comes and goes.
Start off by editing VideoCell.h,
adding a few lines to define a delegate, a protocol the delegate should
implement, and a property declaration for movieViewContainer so that we can reach it from other classes.
// VideoCell.h
#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>
@interface VideoCell : UITableViewCell {
IBOutlet UIView *movieViewContainer;
IBOutlet UILabel *urlLabel;
NSString *urlPath;
MPMoviePlayerController *mpc;
id delegate;
}
@property (retain, nonatomic) UIView *movieViewContainer;
@property (retain, nonatomic) NSString *urlPath;
@property (retain, nonatomic) MPMoviePlayerController *mpc;
@property (assign, nonatomic) id delegate;
+ (NSString *)reuseIdentifier;
+ (CGFloat)rowHeight;
@end
@protocol VideoCellDelegate
- (void)videoCellStartedPlaying:(VideoCell *)cell;
@end
Now switch to VideoCell.m, and add synthesized accessors for movieViewContainer and delegate.
@synthesize urlPath, mpc, movieViewContainer, delegate;
Then free up one additional resource in dealloc:
- (void)dealloc {
self.urlPath = nil;
self.mpc = nil;
self.movieViewContainer = nil;
[super dealloc];
}
Next, implement the following
change, to let the delegate know when the video has been selected. This
way, the view can be shifted to the external screen (if it's connected).
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
if ([delegate respondsToSelector:@selector(videoCellStartedPlaying:)]) {
[delegate videoCellStartedPlaying:self];
}
// Configure the view for the selected state
[mpc play];
}
We
really didn't do too much here. The most interesting part—handling a
user selection that should put the video on the externalScreen—has been foisted off on a vaguely defined delegate object. Let's make that a bit more concrete, by having VideoToyViewController act as the delegate for VideoCell. This controller will now keep track of the selected VideoCell instance, as well as a UIWindow assigned to an external screen, if there is one.
Open VideoToyViewController.h, and make the changes shown here:
// VideoToyViewController.h
#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>
@class VideoCell;
@interface VideoToyViewController : UITableViewController {
NSMutableArray *urlPaths;
IBOutlet VideoCell *videoCell;
UIWindow *externalWindow;
VideoCell *selectedCell;
}
@property (retain, nonatomic) NSMutableArray *urlPaths;
@property (retain, nonatomic) UIWindow *externalWindow;
@property (retain, nonatomic) VideoCell *selectedCell;
@end
Now it's time for VideoToyViewController.m, which is where the real work of managing the external screen happens. Synthesize the new properties, like this:
@synthesize urlPaths, externalWindow, selectedCell;
And make sure that resources are properly freed:
- (void)dealloc {
self.urlPaths = nil;
self.externalWindow = nil;
self.selectedCell = nil;
[super dealloc];
}
Next, move on to the viewDidLoad method. Here, we're going to first call the updateExternalWindow
method (which we'll define in just a moment). We'll also set up
notifications whenever an external screen is connected or disconnected.
Both of these events will also call the updateExternalWindow method.
- (void)viewDidLoad {
[super viewDidLoad];
[self updateExternalWindow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateExternalWindow)
name:UIScreenDidConnectNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateExternalWindow)
name:UIScreenDidDisconnectNotification
object:nil];
}
And now for the updateExternalWindow
method itself. As you just saw, this method is called when our view is
loaded, as well as every time the external screen is connected or
disconnected. It's fairly complicated, since it is designed to handle
the variety of situations it may encounter and do the right thing. The
comments in the code provide more details.
- (void)updateExternalWindow {
if ([[UIScreen screens] count] > 1) {
//
// An external screen is connected. Find the screen, put a
// UIWindow on it.
//
UIScreen *externalScreen = [[UIScreen screens] lastObject];
// Screen modes are sorted in order of increasing resolution.
// Let's take the highest.
UIScreenMode *highestScreenMode = [[externalScreen availableModes]
lastObject];
CGRect externalWindowFrame = CGRectMake(0, 0,
[highestScreenMode size].width, [highestScreenMode size].height);
self.externalWindow = [[[UIWindow alloc] initWithFrame:
externalWindowFrame] autorelease];
externalWindow.screen = externalScreen;
[externalWindow.screen setCurrentMode:highestScreenMode];
[externalWindow makeKeyAndVisible];
if (selectedCell) {
// A cell is selected. Move its view to the external window.
[externalWindow addSubview:selectedCell.mpc.view];
selectedCell.mpc.view.frame = externalWindow.bounds;
}
} else if ([[UIScreen screens] count] == 1) {
//
// No external screen is connected. Let's make sure we have no
// dangling references
// to anything off the main screen.
//
if ([[externalWindow subviews] count] > 0) {
// externalWindow used to be attached to a screen which is no
// longer there! Move its view back to where it came from.
UIView *v = [[externalWindow subviews] lastObject];
v.frame = selectedCell.movieViewContainer.bounds;
[selectedCell.movieViewContainer addSubview:v];
}
self.externalWindow.screen = nil;
self.externalWindow = nil;
}
}
NOTE
The updateExternalWindow
method could have been split into three methods: one for each of the
notifications, and one for after the nib file loaded. In fact, the
first version I wrote did just that. But I noticed there was some
functional overlap, so I refactored a bit. To me, it seems that
compressing it into one method brings it together a bit better.
The
next thing to tackle is the creation of the VideoCell instances. Each
needs to be told who its delegate is. Find the relevant section in
tableView:cellForRowAtIndexPath:, and add the bold line shown here:
[[NSBundle mainBundle] loadNibNamed:@"VideoCell" owner:self
options:nil];
cell = videoCell;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.delegate = self;
2. Implementing the VideoCell Delegate Method
Finally,
let's implement the delegate method itself. As you may recall, this
method is called whenever the user selects a VideoCell
in the GUI. Here, we need to check whether an external screen is
connected and whether another video is currently running. Yes, this
method is even more complicated than the updateExternalWindow method, but it has a lot to do. Again, the code comments provide more explanation.
- (void)videoCellStartedPlaying:(VideoCell *)cell {
if (selectedCell != cell) { // Skip everything if it's the same cell.
if ([[UIScreen screens] count] > 1) {
// Switching external from one video (or blank) to another
UIScreen *externalScreen = [[UIScreen screens] lastObject];
UIScreenMode *highestScreenMode = [[externalScreen availableModes]
lastObject];
CGRect externalWindowFrame = CGRectMake(0, 0,
[highestScreenMode size].width, [highestScreenMode size].height);
if ([[externalWindow subviews] count] > 0) {
// There's already a movie there. Put its view back in the cell
// it came from.
UIView *v = [[externalWindow subviews] lastObject];
v.frame = selectedCell.movieViewContainer.bounds;
[selectedCell.movieViewContainer addSubview:v];
}
// We're done with the old movie and cell.
self.selectedCell = cell;
// Get rid of the old window and screens; create new ones.
self.externalWindow = [[[UIWindow alloc] initWithFrame:
externalWindowFrame] autorelease];
externalWindow.screen = externalScreen;
[externalWindow.screen setCurrentMode:highestScreenMode];
[externalWindow makeKeyAndVisible];
if (selectedCell) {
// Move the selected cell's movie view to the external screen.
[externalWindow addSubview:selectedCell.mpc.view];
selectedCell.mpc.view.frame = externalWindow.bounds;
}
} else if ([[UIScreen screens] count] == 1) {
// No external screen is connected.
if ([[externalWindow subviews] count] > 0) {
// We seem to have an old external window hanging around. Move
// its view back to the cell it came from.
UIView *v = [[externalWindow subviews] lastObject];
v.frame = selectedCell.movieViewContainer.bounds;
[selectedCell.movieViewContainer addSubview:v];
}
self.externalWindow.screen = nil;
self.externalWindow = nil;
// Keep track of the selected cell.
self.selectedCell = cell;
}
}
}
3. Testing the External Screen Functionality
Now build and run the app, and then unplug your iPad from your computer. This will probably crash the app, but that's OK.
Start up the app again
directly on the iPad. Now whip out your trusty iPad-VGA adapter, find a
convenient monitor with a VGA input, and plug it in. If you do that
while a video is playing, you'll see that the video jumps to the
external monitor. Otherwise, start playing a video, and you'll see it
appear there.
The code we wrote for this
functionality is quite robust. You should be able to unplug and
reattach the screen during playback, before starting playback, while
switching songs, before launching the app, and so on. You'll find that
"it just works."